<template>
  <div>
    <header class="layout__col-2-1 header">
      <div v-show="tableLoading" class="header__loading"></div>
      <ul v-if="!tableLoading" class="header__filter">
        <li class="header__filter-item header__filter-item--grids">
          <a
            ref="viewsSelectToggle"
            class="header__filter-link"
            @click="
              $refs.viewsContext.toggle(
                $refs.viewsSelectToggle,
                'bottom',
                'left',
                4
              )
            "
          >
            <span v-if="hasSelectedView">
              <i
                class="header__filter-icon header-filter-icon--view fas"
                :class="'fa-' + view._.type.iconClass"
              ></i>
              {{ view.name }}
            </span>
            <span v-else>
              <i
                class="header__filter-icon header-filter-icon-no-choice fas fa-caret-square-down"
              ></i>
              Choose view
            </span>
          </a>
          <ViewsContext ref="viewsContext" :table="table"></ViewsContext>
        </li>
        <li
          v-if="hasSelectedView && view._.type.canFilter"
          class="header__filter-item"
        >
          <ViewFilter
            :view="view"
            :fields="fields"
            :primary="primary"
            @changed="refresh()"
          ></ViewFilter>
        </li>
        <li
          v-if="hasSelectedView && view._.type.canSort"
          class="header__filter-item"
        >
          <ViewSort
            :view="view"
            :fields="fields"
            :primary="primary"
            @changed="refresh()"
          ></ViewSort>
        </li>
      </ul>
      <component
        :is="getViewHeaderComponent(view)"
        v-if="hasSelectedView"
        :database="database"
        :table="table"
        :view="view"
        :fields="fields"
        :primary="primary"
      />
      <ul v-if="!tableLoading" class="header__info">
        <li>{{ database.name }}</li>
        <li>{{ table.name }}</li>
      </ul>
    </header>
    <div class="layout__col-2-2 content">
      <component
        :is="getViewComponent(view)"
        v-if="hasSelectedView && !tableLoading"
        ref="view"
        :database="database"
        :table="table"
        :view="view"
        :fields="fields"
        :primary="primary"
        @refresh="refresh"
      />
      <div v-if="viewLoading" class="loading-overlay"></div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'

import { StoreItemLookupError } from '@baserow/modules/core/errors'
import ViewsContext from '@baserow/modules/database/components/view/ViewsContext'
import ViewFilter from '@baserow/modules/database/components/view/ViewFilter'
import ViewSort from '@baserow/modules/database/components/view/ViewSort'

/**
 * This page component is the skeleton for a table. Depending on the selected view it
 * will load the correct components into the header and body.
 */
export default {
  components: {
    ViewsContext,
    ViewFilter,
    ViewSort,
  },
  /**
   * When the user leaves to another page we want to unselect the selected table. This
   * way it will not be highlighted the left sidebar.
   */
  beforeRouteLeave(to, from, next) {
    this.$store.dispatch('table/unselect')
    next()
  },
  layout: 'app',
  /**
   * Because there is no hook that is called before the route changes, we need the
   * tableLoading middleware to change the table loading state. This change will get
   * rendered right away. This allows us to have a custom loading animation when
   * switching views.
   */
  middleware: ['tableLoading'],
  /**
   * Prepares all the table, field and view data for the provided database, table and
   * view id.
   */
  async asyncData({ store, params, error, app }) {
    // @TODO figure out why the id's aren't converted to an int in the route.
    const databaseId = parseInt(params.databaseId)
    const tableId = parseInt(params.tableId)
    let viewId = params.viewId ? parseInt(params.viewId) : null
    const data = {}

    // Try to find the table in the already fetched applications by the
    // groupsAndApplications middleware and select that one. By selecting the table, the
    // fields and views are also going to be fetched.
    try {
      const { database, table } = await store.dispatch('table/selectById', {
        databaseId,
        tableId,
      })
      await store.dispatch('group/selectById', database.group.id)
      data.database = database
      data.table = table
    } catch (e) {
      // In case of a network error we want to fail hard.
      if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
        throw e
      }

      return error({ statusCode: 404, message: 'Table not found.' })
    }

    // After selecting the table the fields become available which need to be added to
    // the data.
    data.fields = store.getters['field/getAll']
    data.primary = store.getters['field/getPrimary']

    // Because we do not have a dashboard for the table yet we're going to redirect to
    // the first available view.
    const firstView = store.getters['view/first']
    if (viewId === null && firstView !== null) {
      viewId = firstView.id
    }

    // If a view id is provided and the table is selected we can select the view. The
    // views that belong to the table have already been fetched so we just need to
    // select the correct one.
    if (viewId !== null) {
      try {
        const { view } = await store.dispatch('view/selectById', viewId)
        data.view = view

        // It might be possible that the view also has some stores that need to be
        // filled with initial data so we're going to call the fetch function here.
        const type = app.$registry.get('view', view.type)
        await type.fetch({ store }, view)
      } catch (e) {
        // In case of a network error we want to fail hard.
        if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
          throw e
        }

        return error({ statusCode: 404, message: 'View not found.' })
      }
    }

    return data
  },
  data() {
    return {
      // Shows a small spinning loading animation when the view is being refreshed.
      viewLoading: false,
    }
  },
  head() {
    return {
      title: (this.view ? this.view.name + ' - ' : '') + this.table.name,
    }
  },
  computed: {
    /**
     * Indicates if there is a selected view by checking if the view object has been
     * populated.
     */
    hasSelectedView() {
      return (
        this.view !== undefined &&
        Object.prototype.hasOwnProperty.call(this.view, '_')
      )
    },
    ...mapState({
      // We need the tableLoading state to show a small loading animation when
      // switching between views. Because some of the data will be populated by
      // the asyncData function and some by mapping the state of a store it could look
      // a bit strange for the user when switching between views because not all data
      // renders at the same time. That is why we show this loading animation. Store
      // changes are always rendered right away.
      tableLoading: (state) => state.table.loading,
    }),
  },
  /**
   * The beforeCreate hook is called right after the asyncData finishes and when the
   * page has been rendered for the first time. The perfect moment to stop the table
   * loading animation.
   */
  beforeCreate() {
    this.$store.dispatch('table/setLoading', false)
  },
  beforeMount() {
    this.$bus.$on('table-refresh', this.refresh)
  },
  mounted() {
    this.$realtime.subscribe('table', { table_id: this.table.id })
  },
  beforeDestroy() {
    this.$bus.$off('table-refresh', this.refresh)
    this.$realtime.subscribe(null)
  },
  methods: {
    getViewComponent(view) {
      const type = this.$registry.get('view', view.type)
      return type.getComponent()
    },
    getViewHeaderComponent(view) {
      const type = this.$registry.get('view', view.type)
      return type.getHeaderComponent()
    },
    /**
     * Refreshes the whole view. All data will be reloaded and it will visually look
     * the same as seeing the view for the first time.
     */
    async refresh(event) {
      this.viewLoading = true
      const type = this.$registry.get('view', this.view.type)
      await type.refresh({ store: this.$store }, this.view)
      if (
        Object.prototype.hasOwnProperty.call(this.$refs, 'view') &&
        Object.prototype.hasOwnProperty.call(this.$refs.view, 'refresh')
      ) {
        await this.$refs.view.refresh()
      }
      // It might be possible that the event has a callback that needs to be called
      // after the rows are refreshed. This is for example the case when a field has
      // changed. In that case we want to update the field in the store after the rows
      // have been refreshed to prevent incompatible values in field types.
      if (event && Object.prototype.hasOwnProperty.call(event, 'callback')) {
        await event.callback()
      }
      this.$nextTick(() => {
        this.viewLoading = false
      })
    },
  },
}
</script>
